#!/usr/bin/perl
use strict;
use warnings;
use File::Find;
use Digest::MD5 qw(md5 md5_hex md5_base64);
use Getopt::Long;
use Data::Dumper;

my @SRC = ();
my $TARGET = ();
my $VERBOSE = 0;
my $DELETE = 0;
my $SYNCFILE = '.sync';
my $HELP = 0;

my $TARGET_DIR;

my %STATS = ();
my $STATS = 0; # don't do stats per default.

GetOptions ("src|source=s" => \@SRC,    
	    "target=s"     => \$TARGET, 
	    "verbose+"   => \$VERBOSE,
	    "delete"     => \$DELETE,
	    "syncfile=s" => \$SYNCFILE,
	    "stats"      => \$STATS, # do stats 
	    "help"       => \$HELP,
	    
    ) or die("GetOptions: $!\n");

if ($HELP) {
    print "Syntax: psynco [options]\n",
    "Please use 'perldoc psynco' if you need more help.\n";
    exit 1;
}

die ("You must specify at least one source (--src) and a target (--target).\n")
    unless ((@SRC) or ($TARGET));


# init stats:
$STATS{files_added}     = 0;
$STATS{bytes_added}     = 0;
$STATS{files_deleted}   = 0;
$STATS{bytes_deleted}   = 0;

$STATS{files_in_target} = 0;
$STATS{bytes_in_target} = 0;

opendir(DIR, $TARGET) || die "can’t opendir $TARGET $!";
while (my $f = readdir(DIR)) {
    next if ($f =~ m/^\./);
    m_add('bytes_in_target',"$TARGET/$f") if ($STATS);
    $STATS{files_in_target}++;
    $TARGET_DIR->{"$TARGET/$f"} = 1;
}
closedir(DIR);
print("Recursing into: ", join(', ', @SRC), "\n")
    if ($VERBOSE>1);

find(\&wanted1, @SRC);


if ($VERBOSE and scalar keys %{$TARGET_DIR}) {
    print "Excessive files in target: \n";
}
for my $f (sort keys %{$TARGET_DIR}) {
    print "$f" if ($VERBOSE);
    if ($DELETE) {
	unlink ($f) or die("Unlink: $!\n");
	print " (deleted)" if ($VERBOSE);
	$STATS{files_deleted}++;
	m_add('bytes_deleted',$f);
    }
    print "\n" if ($VERBOSE);
}

# adjust bytes in target. We counted before we deleted...
$STATS{bytes_in_target} = $STATS{bytes_in_target} - $STATS{bytes_deleted} + $STATS{bytes_added};
$STATS{files_in_target} = $STATS{files_in_target} - $STATS{files_deleted} + $STATS{files_added}; 

print("Files added:     $STATS{files_added}\n",
      "Files deleted:   $STATS{files_deleted}\n",
      "Files in target: $STATS{files_in_target}\n",
      "\n",
      "Bytes added:     $STATS{bytes_added}\n",
      "Bytes deleted:   $STATS{bytes_deleted}\n",
      "Bytes in target  $STATS{bytes_in_target}\n",
    ) if ($STATS);

# execution ends here.

sub wanted1 {
    if ($_ eq $SYNCFILE) {
	recurse($File::Find::dir);
    }   
}


sub recurse {
    my ($dir) = @_;
    print "Recursing into $dir\n"
	if ($VERBOSE>1);
    find(\&wanted2, $dir);
}


sub wanted2 {
    if (m/\.mp3$/) {
	hardlink("$File::Find::dir/$_", $_);
    }   
}

sub hardlink {
    my ($file, $target) = @_;
    my $md5 = md5_hex($file); # md5sum of the src

    $target =~ s/.mp3$//;                # strip off the extension. 
    $target = substr($target,0,24,'');   # strip down the long filenames
    $md5 = substr($md5,0,8,'');          # shorten the md5 sum

    $target =~ s/[^a-zA-Z0-9_ ]//g;      # clean the filename
    $target = "$TARGET/$target-$md5.mp3";
    
    delete $TARGET_DIR->{$target};

    unless (-e $target) {
	link ($file, $target); 
	print "hardlinking $file -> $target\n"
	    if $VERBOSE;
	m_add('bytes_added',$file);
	$STATS{files_added}++;
    } else {
	print "Skipping $file - exists\n"
	    if $VERBOSE > 1;
    }
}

sub m_add {
   my ($stat,$f) = @_;
   my $size = (stat($f))[7] if ($STATS);
   $STATS{$stat} +=  $size;
}
__END__

=pod
=head1 NAME

psynco -- synchronize your mp3 files with your pmp.

=head1 SYNOPSIS

psynco [--options]

=head1 DESCRIPTION

psynco helps synchronizes all or parts of your mp3 collection onto
your various media players.

=head1 OPTIONS

=over 5

=item B<--help>

Help!

=item B<--source <directory>>

Source directory.

=item B<--target <directory>>

Target directory. Where the hardlinks are created. Must be on the same
filsystem as the originals.

=item B<--syncfile <filename>>

Specify the dotfile used to indicate that the directory is to be
synchronized to the target. Use different syncfiles in order to
synchronize different directories to different devices.

=item B<--verbose>

Adds verbosity. Can be used several times to increase verbosity.

=item B<--delete>

Delete files in target directory that are no longer present in the
source directories.

=item B<--stats>

Do statistics on how much data is changed and how much is in the target directories. This is 
optional because the stat() system call is heavy.

=back

=head1 EXAMPLES

I use the following three statements in order to keep my mp3s in sync on my pmp, my phone and my GPS. 

psynco --syncfile .pmpsync --src /misc/mm/music --src /misc/mm/podcasts --src /misc/mm/audiobooks --target /misc/mm/sync/zen --delete

psynco --syncfile .phonesync --src /misc/mm/music --src /misc/mm/audiobooks --target /misc/mm/sync/phone --delete

psynco --syncfile .gpssync --src /misc/mm/music --target /misc/mm/sync/gps --delete

Note: I use different tools to do the actual transfer. For my pmp (a zen) I use mtpsync - for the other I use rsync.

=head1 VERSION

psynco 0.03

=head1 COPYRIGHT

Copyright 2008 Per Andreas Buer

This is free software; see the source for copying conditions. There is
NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE.

Please see http://www.gnu.org/licenses/gpl-3.0.txt for details.

=cut
